15. Scheme
Scheme 是 Lisp 的一种方言,而后者是世界上第二古老的编程语言。Lisp 与 Python 一样,是一个典型的 REPL(Read-Eval-Print Loop,“读取-求值-输入” 循环,又被称为交互式顶层构件,Interactive Toplevel) 语言。在一个典型的 REPL 语言中,仅有三个底层函数,这三个函数通过循环运作可以构建出这种语言所有的内置函数与用户编写的任意代码:
read读取函数:接收一些字符串,按照一定的规则解析为某种 抽象语法树,并存入内存中。eval求值函数:接收某个抽象语法树,“执行”它,得到另一些对象。print打印函数:接收某些对象(一般都是eval求值得到的结果)并将其输出给用户。
其本身的语法相当简洁优雅,其基本语法只有以下区区几条:
表达式
在 Scheme 中 万物皆表达式。数值等原子表达式是 自求值的,例如 2、true 等。其余 组合式(Combinations) 通过小括号括起来。除一些 Scheme 语言规定的特殊形式的组合式外,其余组合式都是组合表达式。
组合表达式本质上只是一个 列表。
组合表达式写法上与 Python 不同,看起来是一种 前缀表达式 的写法,标准形式是这样的:(<运算符子表达式> <操作数1> <操作数2> ...)。例如 (+ 1 2)。
组合表达式内部可以任意嵌套更多表达式。Scheme 解释器对换行、缩进、多余空格等 Whitespace 不敏感。
特殊格式
Scheme 规定了如下的特殊形式的组合式:
- 单条件 if 语句:
(if <谓词表达式> <Consequent> <Alternative>)。根据谓词表达式的值决定执行两个表达式中的一个。 - and 与 or 语句:
(and <predicate1> <predicate2> ...)。连接多个谓词表达式,存在短路机制。 - 变量定义语句:
(define <变量名> <值表达式>)。将一个值绑定到一个变量名上。 - 函数定义语句 :
(define (<函数名> <形参>) <函数体表达式>)。定义一个过程并绑定到对应的函数名。 - Lambda 表达式:
(lambda (<形参表>) <函数体表达式>)。定义并返回一个匿名函数表达式。 - 多条件 cond 语句:
(cond (<谓词表达式1> <表达式1>) (<谓词表达式2> <表达式2>)...),类似于 C 中的switch语句。在所有的条件中,可以有零或一个else语句放在最后,类似于default。需要注意的是cond表达式是有值的,其值等于第一个满足的谓词表达式对应的表达式的值。 - begin 语句:
(begin <表达式1> <表达式2>)。允许组合并顺次执行每个表达式,整个大的 begin 表达式的值等于最后一个子表达式的值。 - let 语句:
(let (<变量名> <变量值表达式>) <值表达式>)。生成 仅在 let 表达式内部有效 的临时变量,计算值表达式的值并返回,随后销毁生成的临时变量。
列表
Scheme 只有 列表 一种数据结构,其底部基于 链表,本质上是一个 广义表。Scheme 关于列表有如下的内置方法与属性:
(cons <head_expression> <rest_expression>)创建一个头部等于<head_expression>,后继部分为<rest_expression>的链表。其中<head_expression>可以是 任意元素。<rest_expression>必须为一个列表或。(car <list>)返回一个广义表的 头部元素。(cdr <list>返回一个 列表,其包含该广义表内 除头部元素外的所有后继元素。可以发现这与广义表的定义完全一致!nil代表空链表。
Scheme 还有一些内置的列表处理过程:
(append s t):将列表t的所有内容添加到列表s末尾。(map f s):将函数f作用于列表s中的所有元素一次。(filter p s):将谓词函数p作用于列表s中的所有元素一次,返回一个包含所有满足p(s) == T元素的列表。(apply f s):将列表中的所有元素作为函数f的参数执行一次并返回结果。
引用与符号
quote,引用(Quoting)操作,可简写为',后者是前者的 读取宏(Reader Macro) ,直接指代某个 符号。其实际作用是:对 eval 函数,被 quote 修饰的表达式 不会被 eval 函数求值而原封不动的返回。详细的解释见:What the Hell is Symbolic Computation?。quasiquote,准引用(Quasiquoting)操作,可简写为 "`",与引用操作类似。但不同之处在于,准引用的表达式内部可以通过 解引用(Unquoting)操作unquote,符号为,插入 会按照正常规则求值 的表达式。
宏(Macros)
在 Scheme 中,与 C 类似,宏在程序源代码被真正执行求值前执行,负责将带有宏的特殊程序代码依据该宏定义的转换规则转换出可以直接运行求值的代码。通过宏,用户可以自行定义大量的特殊格式的代码。
宏的定义方法如下:
(define-macro (<macro-name> <paras>) (<macro-suite>))
宏的执行过程是:解释器对带有宏的表达式求值,当解释器发现宏名时,与常规表达式的求值过程不同,其会在 不对操作数求值 的情况下直接调用定义的宏过程,得到一个新的表达式后再对整个表达式求值。
例如:
(define-macro (twice expr) (list `begin expr expr))
(twice (print 2))
其实际执行会先将带有宏的表达式转换为普通的表达式:
(begin (print 2) (print 2))
然后再执行,输出两个 2。
可以预见的是,如果 twice 不是一个宏而是普通函数表达式,则 print 2 会先于函数体运行,只输出一个 2。